﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Data;
using System.Data.Metadata.Edm;
using System.Dynamic;
using System.Text.RegularExpressions;

namespace JsonRestObjectContext.Controllers
{
    public class ObjectContextJsonRestController : Controller
    {
        ObjectContext db;
        string store = "store";
        bool absRef = true;
        System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

        public ObjectContext ObjectContext { get { return db; } set { db = value; } }
        public string StoreRouteValueKey { get { return store; } set { store = value; } }
        public bool UseAbsolutePathRef { get { return absRef; } set { absRef = value; } }

        IDictionary<string, object> PropertiesAsDictionary(object source)
        {
            var properties = new Dictionary<string, object>();
            foreach (var property in source.GetType().GetProperties().Where(p => p.CanRead))
            {
                var key = property.Name;
                var value = property.GetValue(source, null);
                properties[key] = value;
            }
            return properties;
        }

        object getObjectSet(string setName)
        {
            return PropertiesAsDictionary(db)[setName];
        }
        EntitySet getSetMeta(string setName)
        {
            return ((dynamic)getObjectSet(setName)).EntitySet;
        }
        void setProperties(string setName, object dest, dynamic src)
        {
            var meta = getSetMeta(setName);
            foreach (var name in meta.ElementType.Properties
                .Select(pr => pr.Name)
                .Except(meta.ElementType.KeyMembers.Select(km => km.Name)))
            {
                var prop = dest.GetType().GetProperty(name);
                if (src.ContainsKey(name))
                {
                    prop.SetValue(dest, src[name], null);
                }
            }
            foreach (var prop in meta.ElementType.NavigationProperties)
            {
                var set = (dynamic)dest.GetType().GetProperty(prop.Name).GetValue(dest, null);
                var setType = ((Type)set.GetType()).GetGenericArguments()[0];
                var keyList = new List<EntityKey>();
                if (src.ContainsKey(prop.Name))
                {
                    foreach (var entry in src[prop.Name])
                    {
                        EntityKey key = getEntityKeyForPath(entry["$ref"]);
                        dynamic result;
                        if (db.TryGetObjectByKey(key, out result))
                        {
                            if (result.GetType().Equals(setType))
                            {
                                keyList.Add(key);
                                if (!set.Contains(result))
                                {
                                    set.Add(result);
                                }
                            }
                        }
                    }
                }
                if (set.Count > 0)
                {
                    var setArr = Array.CreateInstance(setType, set.Count);
                    set.CopyTo(setArr, 0);
                    foreach (var entry in setArr)
                    {
                        if (!keyList.Contains(entry.EntityKey))
                        {
                            set.Remove(entry);
                        }
                    }
                }
            }
        }

        EntityKey getEntityKeyForPath(string path)
        {
            var match = Regex.Match(path, @"/(?<set>[^/]+)/(?<id>[^/]+)$");
            return getEntityKey(match.Groups["set"].Value, match.Groups["id"].Value);
        }

        EntityKey getEntityKey(string setName, string id)
        {
            var meta = getSetMeta(setName);
            var key = meta.ElementType.KeyMembers[0];
            var type = ((PrimitiveType)key.TypeUsage.EdmType).ClrEquivalentType;
            var ent = new EntityKey(meta.EntityContainer.Name + "." + meta.Name, key.Name, Convert.ChangeType(id, type));
            return ent;

        }
        string getPath(EntityKey key, bool forceAbsolute = false)
        {
            var m = Regex.Match((forceAbsolute || absRef) ? Request.Url.AbsoluteUri : Request.Path, "(.+/)[^/]+/[^/]*");
            var s = m.Groups[1].Value;
            var k = key.EntityKeyValues[0].Value.ToString();
            return s + key.EntitySetName + "/" + k;
        }


        IDictionary<string, object> getLazy(string setName, object obj)
        {
            var dict = PropertiesAsDictionary(obj);
            var res = new Dictionary<string, object>();
            var meta = getSetMeta(setName);
            var q = from p in meta.ElementType.Properties
                    select new { name = p.Name, value = dict[p.Name] };
            foreach (var s in q)
            {
                res.Add(s.name, s.value);
            }
            var n = from p in meta.ElementType.NavigationProperties
                    select new
                    {
                        name = p.Name,
                        single = dict[p.Name].GetType().IsGenericType ? null : ((dynamic)dict[p.Name]).EntityKey,
                        coll = dict[p.Name].GetType().IsGenericType ? from e in ((IEnumerable<dynamic>)dict[p.Name]) select e.EntityKey : null
                    };
            foreach (var e in n)
            {

                if (e.single != null)
                {
                    var k = new Dictionary<string, object>();
                    k.Add("$ref", getPath(e.single));
                    res.Add(e.name, k);

                }
                if (e.coll != null)
                {
                    var i = new List<IDictionary<string, object>>();
                    foreach (var ee in e.coll)
                    {
                        var k = new Dictionary<string, object>();
                        k.Add("$ref", getPath(ee));
                        i.Add(k);
                    }
                    res.Add(e.name, i);
                }

            }

            return res;
        }

        string getContent()
        {
            return new System.IO.StreamReader(this.Request.InputStream).ReadToEnd();
        }

        public JsonResult Get(string id)
        {
            var setName = RouteData.Values[store].ToString();
            object result;
            if (id != null)
            {
                var key = getEntityKey(setName, id);
                db.TryGetObjectByKey(key, out result);
                var data = getLazy(setName, result);
                return Json(data, JsonRequestBehavior.AllowGet);
            }
            var q = from e in ((IEnumerable<dynamic>)getObjectSet(setName)) select getLazy(setName, e);
            var sortStr = Request.QueryString.ToString().Split('&').FirstOrDefault(s => s.StartsWith("sort("));
            if (sortStr != null)
            {
                sortStr = Uri.UnescapeDataString(sortStr);
                bool thenBy = false;
                MatchCollection mc = Regex.Matches(sortStr, @"(?<dir>[+-])(?<fld>[^\,\)]*),?", RegexOptions.None);
                for (int i = 0; i < mc.Count; i++)
                {
                    var m = mc[i];
                    var fld = m.Groups["fld"].Value;
                    var dir = m.Groups["dir"].Value;

                    if (thenBy)
                    {
                        switch (dir)
                        {
                            case "+": q = ((IOrderedEnumerable<dynamic>)q).ThenBy(e => e[fld]); break;
                            case "-": q = ((IOrderedEnumerable<dynamic>)q).ThenByDescending(e => e[fld]); break;

                        }
                    }
                    else
                    {
                        switch (m.Groups["dir"].Value)
                        {
                            case "+": q = q.OrderBy(e => e[fld]); break;
                            case "-": q = q.OrderByDescending(e => e[fld]); break;

                        }
                        thenBy = true;
                    }
                }
            }
            return Json(q, JsonRequestBehavior.AllowGet);

        }

        public JsonResult Put(string id)
        {
            var setName = RouteData.Values[store].ToString();
            object current;
            var key = getEntityKey(setName, id);
            db.TryGetObjectByKey(key, out current);
            var update = serializer.Deserialize(getContent(), typeof(Dictionary<string, object>));
            setProperties(setName, current, update);
            db.SaveChanges();
            return Json(getLazy(setName, current), JsonRequestBehavior.AllowGet);
        }
        public JsonResult Post(string id)
        {
            var setName = RouteData.Values[store].ToString();
            var objectSet = (dynamic)getObjectSet(setName);
            var current = objectSet.CreateObject();
            var update = serializer.Deserialize(getContent(), typeof(Dictionary<string, object>));
            setProperties(setName, current, update);
            objectSet.AddObject(current);
            db.SaveChanges();
            Response.AddHeader("Location", getPath(current.EntityKey, true));
            return Json(getLazy(setName, current), JsonRequestBehavior.AllowGet);
        }
        public JsonResult Delete(string id)
        {
            var setName = RouteData.Values[store].ToString();
            object current;
            var key = getEntityKey(setName, id);
            db.TryGetObjectByKey(key, out current);
            db.DeleteObject(current);
            db.SaveChanges();
            return Json(getLazy(setName, current), JsonRequestBehavior.AllowGet);
        }
    }
}
